import pickle
import time
import bpy
from ogreEx.all import uid
from ogreEx.axis import swap
from ogreEx.context import getTundraSingleton



def _create_stream_proto():
    proto = {}
    tags = 'ID NAME POSITION ROTATION SCALE DATA SELECTED TYPE MESH LAMP CAMERA SPEAKER ANIMATIONS DISTANCE ENERGY VOLUME MUTE LOD'.split()
    for i,tag in enumerate( tags ):
        proto[ tag ] = chr(i) # up to 256
    return proto
    
STREAM_PROTO = _create_stream_proto()
STREAM_BUFFER_SIZE = 2048

## Server object to talk with realXtend Tundra with UDP
## Requires Tundra to be running a py script.

class Server(object):
    def stream( self, o ):
        b = pickle.dumps( o, protocol=2 ) #protocol2 is python2 compatible
        #print( 'streaming bytes', len(b) )
        n = len( b ); d = STREAM_BUFFER_SIZE - n -4
        if n > STREAM_BUFFER_SIZE:
            print( 'ERROR: STREAM OVERFLOW:', n )
            return
        padding = b'#' * d
        if n < 10: header = '000%s' %n
        elif n < 100: header = '00%s' %n
        elif n < 1000: header = '0%s' %n
        else: header = '%s' %n
        header = bytes( header, 'utf-8' )
        assert len(header) == 4
        w = header + b + padding
        assert len(w) == STREAM_BUFFER_SIZE
        self.buffer.insert(0, w )
        return w

    def multires_lod( self ):
        '''
        Ogre builtin LOD sucks for character animation
        '''
        ob = bpy.context.active_object
        cam = bpy.context.scene.camera

        if ob and cam and ob.type=='MESH' and ob.use_multires_lod:
            delta = bpy.context.active_object.matrix_world.to_translation() - cam.matrix_world.to_translation()
            dist = delta.length
            #print( 'Distance', dist )
            if ob.modifiers and ob.modifiers[0].type == 'MULTIRES' and ob.modifiers[0].total_levels > 1:
                mod = ob.modifiers[0]
                step = ob.multires_lod_range / mod.total_levels
                level = mod.total_levels - int( dist / step )
                if mod.levels != level: mod.levels = level
                return level

    def sync( self ):   # 153 bytes per object + n bytes for animation names and weights
        LOD = self.multires_lod()

        p = STREAM_PROTO
        i = 0; msg = []
        for ob in bpy.context.selected_objects:
            if ob.type not in ('MESH','LAMP','SPEAKER'): continue
            loc, rot, scale = ob.matrix_world.decompose()
            loc = swap(loc).to_tuple()
            x,y,z = swap( rot.to_euler() )
            rot = (x,y,z)
            x,y,z = swap( scale )
            scale = ( abs(x), abs(y), abs(z) )
            d = { p['ID']:uid(ob), p['POSITION']:loc, p['ROTATION']:rot, p['SCALE']:scale, p['TYPE']:p[ob.type] }
            msg.append( d )

            if ob.name == bpy.context.active_object.name and LOD is not None:
                d[ p['LOD'] ] = LOD

            if ob.type == 'MESH':
                arm = ob.find_armature()
                if arm and arm.animation_data and arm.animation_data.nla_tracks:
                    anim = None
                    d[ p['ANIMATIONS'] ] = state = {}    # animation-name : weight
                    for nla in arm.animation_data.nla_tracks:
                        for strip in nla.strips:
                            if strip.active: state[ strip.name ] = strip.influence
                else: pass      # armature without proper NLA setup
            elif ob.type == 'LAMP':
                d[ p['ENERGY'] ] = ob.data.energy
                d[ p['DISTANCE'] ] = ob.data.distance
            elif ob.type == 'SPEAKER':
                d[ p['VOLUME'] ] = ob.data.volume
                d[ p['MUTE'] ] = ob.data.muted

            if i >= 10: break    # max is 13 objects to stay under 2048 bytes
        return msg

    def __init__(self):
        import socket
        self.buffer = []    # cmd buffer
        self.socket = sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)   # UDP
        host='localhost'; port = 9420
        sock.connect((host, port))
        print('SERVER: socket connected', sock)
        self._handle = None
        self.setup_callback( bpy.context )
        import threading
        self.ready = threading._allocate_lock()
        self.ID = threading._start_new_thread(
            self.loop, (None,)
        )
        print( 'SERVER: thread started')

    def loop(self, none):
        self.active = True
        prev = time.time()
        while self.active:
            if not self.ready.locked(): time.sleep(0.001)    # not threadsafe
            else:    # threadsafe start
                now = time.time()
                if now - prev > 0.066:            # don't flood Tundra
                    actob = None
                    try: actob = bpy.context.active_object
                    except: pass
                    if not actob: continue
                    prev = now
                    sel = bpy.context.active_object
                    msg = self.sync()
                    self.ready.release()          # thread release
                    self.stream( msg )            # releases GIL?
                    if self.buffer:
                        bin = self.buffer.pop()
                        try:
                            self.socket.sendall( bin )
                        except:
                            print('SERVER: send data error')
                            time.sleep(0.5)
                            pass
                    else: print( 'SERVER: empty buffer' )
                else:
                    self.ready.release()
        print('SERVER: thread exit')

    def threadsafe( self, reg ):
        if not getTundraSingleton(): return
        if not self.ready.locked():
            self.ready.acquire()
            time.sleep(0.0001)
            while self.ready.locked():    # must block to be safe
                time.sleep(0.0001)            # wait for unlock
        else: pass #time.sleep(0.033) dont block

    _handle = None
    def setup_callback( self, context ):        # TODO replace with a proper frame update callback
        print('SERVER: setup frame update callback')
        if self._handle: return self._handle
        for area in bpy.context.window.screen.areas:
            if area.type == 'VIEW_3D':
                for reg in area.regions:
                    if reg.type == 'WINDOW':
                        # PRE_VIEW, POST_VIEW, POST_PIXEL
                        self._handle = reg.callback_add(self.threadsafe, (reg,), 'PRE_VIEW' )
                        self._area = area
                        self._region = reg
                        break
        if not self._handle:
            print('SERVER: FAILED to setup frame update callback')